feat: gif support for profile picture#1366
Conversation
There was a problem hiding this comment.
Pull request overview
Adds animated GIF support for profile pictures (~/.face) by introducing a format-detecting ProfileImage component that switches between animated and static rendering.
Changes:
- Add
gifto supported image types/extensions for the file picker. - Introduce
CUtils::imageFormat()(QImageReader-based) for extension-independent format detection. - Replace
CachingImageusage withProfileImagein dashboard and lock screen profile-picture views.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/Images.qml | Extends allowed image extensions/types to include GIF. |
| plugin/src/Caelestia/cutils.hpp | Exposes new QML-invokable imageFormat() helper. |
| plugin/src/Caelestia/cutils.cpp | Implements format detection via QImageReader::imageFormat(). |
| modules/lock/Center.qml | Switches lock-screen avatar rendering to ProfileImage. |
| modules/dashboard/dash/User.qml | Switches dashboard avatar to ProfileImage and triggers reload after picker accept. |
| components/images/ProfileImage.qml | New loader-based component selecting AnimatedImage vs CachingImage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| function reload(): void { | ||
| _format = CUtils.imageFormat(path); | ||
| loader.active = false; | ||
| loader.active = true; | ||
| } | ||
|
|
There was a problem hiding this comment.
reload() assigns to _format, which removes the existing QML binding (property string _format: CUtils.imageFormat(path)). After the first reload, _format will no longer update automatically when path changes, so the component can get stuck using the wrong renderer. Consider keeping _format as a binding (e.g., reassign via Qt.binding(...)) or refactor so reload forces a reload without breaking the binding.
| fillMode: AnimatedImage.PreserveAspectCrop | ||
| asynchronous: true | ||
| playing: true | ||
| source: `file://${root.path}` |
There was a problem hiding this comment.
AnimatedImage.source is built as file://${root.path}, which can produce malformed file URLs for absolute paths (e.g. file:////home/...) and won’t URL-escape characters like spaces. Elsewhere in the codebase AnimatedImage.source is set to a plain local path string (e.g. Paths.absolutePath(...)), which avoids these issues. Prefer using the local path directly (or a proper file-URL constructor) here.
| source: `file://${root.path}` | |
| source: root.path |
|
CI failures are pre-existing and unrelated to this PR:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| property string path | ||
| property string _format: CUtils.imageFormat(path) | ||
|
|
||
| readonly property int status: (loader.item as Image)?.status ?? Image.Null |
There was a problem hiding this comment.
status is derived via (loader.item as Image)?.status, but the loaded item can be an AnimatedImage (not an Image), causing the cast to fail and status to stay Image.Null for GIFs (placeholder will never hide). Use loader.item?.status ?? Image.Null (optionally suppress qmllint like other image wrappers do).
| readonly property int status: (loader.item as Image)?.status ?? Image.Null | |
| readonly property int status: loader.item?.status ?? Image.Null |
| fillMode: AnimatedImage.PreserveAspectCrop | ||
| asynchronous: true | ||
| playing: true | ||
| source: Qt.resolvedUrl(`file://${root.path}`) |
There was a problem hiding this comment.
The AnimatedImage source URL is built as file://${root.path}, which produces malformed URLs for normal absolute paths (e.g., /home/... becomes file:////home/...) and can also mis-handle relative paths (treated as a host). Prefer generating a proper local-file URL (e.g., resolve the local path directly, or use a helper that produces file:///...).
| source: Qt.resolvedUrl(`file://${root.path}`) | |
| source: Qt.resolvedUrl(root.path) |
| AnimatedImage { | ||
| fillMode: AnimatedImage.PreserveAspectCrop | ||
| asynchronous: true | ||
| playing: true | ||
| source: Qt.resolvedUrl(`file://${root.path}`) | ||
| } | ||
| } | ||
|
|
||
| Component { | ||
| id: cachingComponent | ||
|
|
||
| CachingImage { | ||
| path: root.path | ||
| } |
There was a problem hiding this comment.
The loaded AnimatedImage/CachingImage items are not sized to the Loader (no anchors.fill: parent / width+height binding), so the profile image may render at its implicit size instead of filling the avatar container. Add sizing/anchors inside both components so they always fill root like the previous direct CachingImage usage did.
| Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; | ||
| Q_INVOKABLE bool deleteFile(const QUrl& path) const; | ||
| Q_INVOKABLE QString toLocalFile(const QUrl& url) const; | ||
| Q_INVOKABLE QString imageFormat(const QString& path) const; |
There was a problem hiding this comment.
New CUtils::imageFormat takes a raw QString path, unlike the other filesystem-related helpers here which take QUrl and validate isLocalFile(). Consider switching this API to QUrl (and rejecting non-local URLs) so QML call sites are consistent and you don’t accidentally pass a qrc://remote URL string that will always return an empty format.
| Q_INVOKABLE QString imageFormat(const QString& path) const; | |
| Q_INVOKABLE QString imageFormat(const QUrl& url) const; | |
| QString imageFormat(const QString& path) const; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fillMode: AnimatedImage.PreserveAspectCrop | ||
| asynchronous: true | ||
| playing: true | ||
| source: Qt.resolvedUrl(`file://${root.path}`) |
There was a problem hiding this comment.
AnimatedImage.source is built via Qt.resolvedUrl(file://${root.path}), which can produce non-canonical/invalid file URLs for absolute paths (e.g., file:////home/...) and differs from existing AnimatedImage usage in the repo (which passes an absolute local path string). Prefer using Qt.resolvedUrl(root.path) (no manual file:// prefix) or pass root.path directly to let QML convert the local path to a URL correctly.
| source: Qt.resolvedUrl(`file://${root.path}`) | |
| source: Qt.resolvedUrl(root.path) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| readonly property list<string> validImageTypes: ["jpeg", "png", "webp", "tiff", "svg", "gif"] | ||
| readonly property list<string> validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg", "gif"] |
There was a problem hiding this comment.
Adding gif to the global validImageExtensions affects every FileDialog using this list (e.g., wallpaper picker) and will allow selecting GIFs even where only static rendering is supported. Consider scoping this to profile-picture selection (separate list) or ensuring all consumers can handle GIFs appropriately.
63b6188 to
93d8368
Compare
13d7d2b to
0148a73
Compare
|
Quick update: I rebased the PR on top of the latest upstream changes after the new image cache provider merge. I tested an additional follow-up that moved image format detection into The current branch keeps the stable behavior:
I’ll keep the PR focused on the working GIF support first, and can revisit deeper integration with the new image utils/cache provider separately if needed. |
|
Updated this PR after the upstream image cache provider changes. The current version keeps GIF profile pictures on Follow-up polish included:
I intentionally avoided changing global Tested on my NixOS setup by rebuilding Caelestia Shell and selecting/replacing GIF profile pictures. Everything works as expected. |
Summary
~/.face)ProfileImagecomponent that detects image format via magic bytes (QImageReader) and switches betweenAnimatedImage(GIF) andCachingImage(static).giffiles directlyChanges
CUtils::imageFormat()— new C++ method for format detection without file extensioncomponents/images/ProfileImage.qml— Loader-based component switching between animated/static renderingUser.qml(dashboard) andCenter.qml(lock screen) to useProfileImagegifto valid image types inImages.qmlgif.mp4